Skip to content

Add shopify store auth and store execute#7122

Open
dmerand wants to merge 2 commits intomainfrom
store-execute
Open

Add shopify store auth and store execute#7122
dmerand wants to merge 2 commits intomainfrom
store-execute

Conversation

@dmerand
Copy link
Copy Markdown
Contributor

@dmerand dmerand commented Mar 27, 2026

What

Add shopify store auth and shopify store execute so Shopify CLI can authenticate an app against a store and then run Admin API GraphQL without needing a local app project in the current repo.

  • shopify store auth starts a PKCE OAuth flow and stores online per-user auth for later store commands.
  • shopify store execute runs Admin API GraphQL against that stored auth.
  • Writes require --allow-mutations.
  • The command surface is aligned with shopify app execute where it makes sense.

Why

We want a clear store workflow in CLI:

  1. authenticate the app against a store
  2. verify access
  3. run store-scoped Admin API operations

This makes store operations usable outside an app project while still using standard app auth and explicit mutation safety.

How

shopify store auth

Add a new shopify store auth command that:

  • authenticates with PKCE (response_type=code, code_challenge, code_challenge_method=S256, code_verifier)
  • listens on a loopback callback bound to 127.0.0.1
  • validates the callback store and state before exchanging the code
  • stores the resulting online token, refresh token, expiry metadata, scopes, and associated user details for reuse by later store commands

shopify store execute

Add a new shopify store execute command that:

  • accepts query / query-file / variables / variable-file / version / output-file flags similar to shopify app execute
  • parses and validates the GraphQL operation before execution
  • blocks mutations unless --allow-mutations is passed
  • fails the command on GraphQL errors instead of treating them as successful output

Session handling

Store auth is persisted per store and per user for the configured app client ID.

When stored auth is expired, shopify store execute refreshes it before version resolution / execution. When stored auth is no longer valid, the current stored session is cleared and the user is prompted to re-run shopify store auth.

That invalid-auth path is handled consistently for:

  • refresh failures
  • invalid refresh responses
  • version-discovery auth failures
  • execute-time 401s

Internal structure

The execute path is split into request preparation, auth/context loading, transport, and result emission helpers so the command stays thin and the execution flow is easier to reason about.

The implementation also adds a small internal target seam for future store-scoped GraphQL APIs while keeping the current behavior Admin-only.

Manual testing

pnpm run shopify store auth   --store shop.myshopify.com   --scopes read_products,write_products

pnpm run shopify store execute   --store shop.myshopify.com   --query 'query { shop { name id } }'

pnpm run shopify store execute   --store shop.myshopify.com   --query 'mutation { shop { id } }'
# should fail until --allow-mutations is added

Copy link
Copy Markdown
Contributor Author

dmerand commented Mar 27, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 82.35% 15352/18642
🟡 Branches 74.75% 7530/10074
🟢 Functions 81.48% 3853/4729
🟢 Lines 82.75% 14517/17543

Test suite run success

4032 tests passing in 1541 suites.

Report generated by 🧪jest coverage report action from 7aae5d3

@dmerand
Copy link
Copy Markdown
Contributor Author

dmerand commented Mar 27, 2026

/snapit

@github-actions
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @dmerand! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260327162821

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

@dmerand dmerand force-pushed the store-execute branch 2 times, most recently from e15c53b to 893af23 Compare March 27, 2026 16:59
Copy link
Copy Markdown
Contributor Author

dmerand commented Mar 27, 2026

/snapit

@github-actions
Copy link
Copy Markdown
Contributor

🫰✨ Thanks @dmerand! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260327203223

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

@dmerand dmerand force-pushed the store-execute branch 2 times, most recently from 97030c7 to 0a0c94d Compare March 31, 2026 17:35
@dmerand dmerand changed the title Proto: shopify store execute Add shopify store auth and store execute Mar 31, 2026
@dmerand dmerand force-pushed the store-execute branch 4 times, most recently from 3d0c271 to c83e839 Compare March 31, 2026 22:36
@dmerand dmerand marked this pull request as ready for review March 31, 2026 22:37
@dmerand dmerand requested review from a team as code owners March 31, 2026 22:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “store” workflow to Shopify CLI that can authenticate an app against a store (PKCE, per-user online tokens) and then execute Admin API GraphQL operations without requiring a local app project.

Changes:

  • Introduces shopify store auth (PKCE OAuth to loopback callback; persists online auth per store/user).
  • Introduces shopify store execute (Admin API GraphQL execution with variable/query file support, mutation safety via --allow-mutations, and error/failure handling).
  • Adds supporting session storage, context/version resolution, transport helpers, and CLI docs/topic updates.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/cli/src/index.ts Registers store:auth and store:execute commands with the CLI entrypoint.
packages/cli/src/cli/commands/store/auth.ts New shopify store auth command wiring/flags.
packages/cli/src/cli/commands/store/auth.test.ts Tests flag parsing and service invocation for store auth.
packages/cli/src/cli/commands/store/execute.ts New shopify store execute command wiring/flags.
packages/cli/src/cli/commands/store/execute.test.ts Tests flag parsing and service invocation for store execute.
packages/cli/src/cli/services/store/auth-config.ts Defines store auth client id, callback path/port, session keying, token masking.
packages/cli/src/cli/services/store/session.ts Implements persisted per-store/per-user session bucket and expiry helper.
packages/cli/src/cli/services/store/session.test.ts Unit tests for session bucket behavior and expiry margin logic.
packages/cli/src/cli/services/store/auth.ts Implements PKCE flow, loopback callback server, token exchange, and persistence.
packages/cli/src/cli/services/store/auth.test.ts Unit tests for PKCE helpers, callback server, token exchange, and persistence.
packages/cli/src/cli/services/store/execute-request.ts Parses query/query-file, variables, validates single operation and mutation gating.
packages/cli/src/cli/services/store/execute-request.test.ts Unit tests for request preparation and validation errors.
packages/cli/src/cli/services/store/admin-graphql-context.ts Loads/refreshes stored auth and resolves Admin API version.
packages/cli/src/cli/services/store/admin-graphql-context.test.ts Tests refresh flow, invalid auth paths, and version selection/validation.
packages/cli/src/cli/services/store/admin-graphql-transport.ts Executes Admin GraphQL request and normalizes 401/GraphQL-error handling.
packages/cli/src/cli/services/store/admin-graphql-transport.test.ts Tests success, 401 clearing + reauth message, GraphQL errors, and passthrough errors.
packages/cli/src/cli/services/store/graphql-targets.ts Adds an internal target seam (currently Admin-only) for store-scoped GraphQL APIs.
packages/cli/src/cli/services/store/graphql-targets.test.ts Tests target context preparation and execution delegation.
packages/cli/src/cli/services/store/execute-result.ts Writes results to file or stdout and emits a success UI message.
packages/cli/src/cli/services/store/execute-result.test.ts Tests file output vs stdout output behavior.
packages/cli/src/cli/services/store/execute.ts Orchestrates request prep → context load → execution → result emission.
packages/cli/src/cli/services/store/execute.test.ts End-to-end-ish unit tests for execute flow behaviors and error cases.
packages/cli/package.json Adds a new store topic/category for CLI command grouping.
packages/cli/README.md Adds docs entries for new commands (and includes regenerated usage sections).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

-n, --name=<value> [env: SHOPIFY_FLAG_NAME] The name for the new app. When provided, skips the app
selection prompt and creates a new app with this name.
-p, --path=<value> [default: ., env: SHOPIFY_FLAG_PATH]
-p, --path=<value> [default: /Users/donald/src/github.com/Shopify/cli/packages/cli, env:
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated docs include a machine-specific absolute path as the default for --path (/Users/donald/...). This is not portable and should not be committed; it should remain a generic default (e.g. .) or whatever the command’s real default is without embedding the generator’s working directory. Re-generate the README after fixing the docs generation/default handling so it doesn’t capture local filesystem paths.

Suggested change
-p, --path=<value> [default: /Users/donald/src/github.com/Shopify/cli/packages/cli, env:
-p, --path=<value> [default: ., env:

Copilot uses AI. Check for mistakes.
EXAMPLES
$ shopify store execute --store shop.myshopify.com --query "query { shop { name } }"

$ shopify store execute --store shop.myshopify.com --query-file ./operation.graphql --variables "{"id":"gid://shopify/Product/1"}"
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example’s JSON quoting will break in most shells because the inner double-quotes aren’t escaped. Update the README example to use proper quoting/escaping (e.g. single quotes around the JSON or escaped double-quotes) so it can be copy/pasted successfully.

Suggested change
$ shopify store execute --store shop.myshopify.com --query-file ./operation.graphql --variables "{"id":"gid://shopify/Product/1"}"
$ shopify store execute --store shop.myshopify.com --query-file ./operation.graphql --variables '{"id":"gid://shopify/Product/1"}'

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +47
const storedBucket = storage.get(storeAuthSessionKey(store))
if (!storedBucket) return undefined

return storedBucket.sessionsByUserId[storedBucket.currentUserId]
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStoredStoreAppSession assumes the persisted bucket always has the expected shape. If the local storage entry is corrupted or from an older schema (e.g. missing sessionsByUserId/currentUserId), this will throw at runtime when running store execute. Consider validating the loaded value (type/field checks) and returning undefined (optionally deleting the bad key) instead of indexing into it unconditionally.

Suggested change
const storedBucket = storage.get(storeAuthSessionKey(store))
if (!storedBucket) return undefined
return storedBucket.sessionsByUserId[storedBucket.currentUserId]
const key = storeAuthSessionKey(store)
const storedBucket = storage.get(key)
if (!storedBucket || typeof storedBucket !== 'object') return undefined
const {sessionsByUserId, currentUserId} = storedBucket as Partial<StoredStoreAppSessionBucket>
if (
!sessionsByUserId ||
typeof sessionsByUserId !== 'object' ||
typeof currentUserId !== 'string'
) {
// Stored data is from an older schema or is corrupted; clear it and treat as no session.
storage.delete(key)
return undefined
}
const session = (sessionsByUserId as {[userId: string]: StoredStoreAppSession})[currentUserId]
if (!session) {
// Current user ID does not map to a stored session; clear invalid bucket.
storage.delete(key)
return undefined
}
return session

Copilot uses AI. Check for mistakes.

export function isSessionExpired(session: StoredStoreAppSession): boolean {
if (!session.expiresAt) return false
return new Date(session.expiresAt).getTime() - EXPIRY_MARGIN_MS < Date.now()
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSessionExpired treats an invalid expiresAt timestamp as “not expired” because new Date(expiresAt).getTime() becomes NaN and the comparison returns false. To avoid accidentally using a bad/unclear expiry value, treat invalid dates as expired (or clear the stored session) so the CLI refreshes/re-auths instead of proceeding with a likely-invalid token.

Suggested change
return new Date(session.expiresAt).getTime() - EXPIRY_MARGIN_MS < Date.now()
const expiresAtMs = new Date(session.expiresAt).getTime()
if (Number.isNaN(expiresAtMs)) return true
return expiresAtMs - EXPIRY_MARGIN_MS < Date.now()

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

@dmerand dmerand force-pushed the store-execute branch 6 times, most recently from f079ab2 to 870584e Compare March 31, 2026 23:28
@@ -0,0 +1,168 @@
import {fetchApiVersions} from '@shopify/cli-kit/node/api/admin'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a lot of different concerns going on in this services area. it'd be nice to establish some clearer patterns of organization around things.

outputContent`Refreshing expired token for ${outputToken.raw(session.store)} (expired at ${outputToken.raw(session.expiresAt ?? 'unknown')}, refresh_token=${outputToken.raw(maskToken(session.refreshToken))})`,
)

const response = await fetch(endpoint, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be nice to split out the gql scaffolding from the specific queries/mutations

} catch (error) {
if (
error instanceof AbortError &&
error.message.includes(`Error connecting to your store ${adminSession.storeFqdn}:`) &&
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a code comment to address the string parsing later?

[key: string]: StoredStoreAppSessionBucket
}

let _storeSessionStorage: LocalStorage<StoreSessionSchema> | undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this gets initialized as part of some lifecycle and not inlined

}

export function getStoreGraphQLTarget(api: StoreGraphQLApi): StoreGraphQLTarget<AdminStoreGraphQLContext> {
switch (api) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what other targets are we going to be supporting?

allowMutations: input.allowMutations,
})

const context = await renderSingleTask({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated as it's an existing pattern, but i really want us to split out rendering and data flow more

Copy link
Copy Markdown
Contributor

@ryancbahan ryancbahan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code generally makes sense. i think there are some improvements we can make to organization, and generally to establishing more clarity around what goes where.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants